Imports System
Imports System.IO
Imports System.Drawing


Partial Class _Default
    Inherits System.Web.UI.Page

    ' Function type
    Public Enum fnType
        STREETLIST
        PLACELIST
        CREDITS
    End Enum
    Public Enum outType
        XML
        JSON
    End Enum

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        ' On Load, carry out action

        ' Check if running locally (for debugging)
        Dim bIsLocal As Boolean = HttpContext.Current.Request.Url.AbsoluteUri.StartsWith("http://localhost:")

        ' Set up database connection 
        Dim MyConn As OleDbConnection

        'Declare database connection variables
        Dim strConn As String = ConfigurationManager.ConnectionStrings("LocalConnectionString").ConnectionString
        MyConn = New OleDbConnection(strConn)

        ' Initialise
        Dim nErrNo As Integer = 0
        Dim sUser As String = "", sPassword As String = "", sText As String = ""
        Dim func As fnType
        Dim frmOut As outType
        Dim nLimit As Integer = 0
        Dim ptCentre As PointF
        ptCentre = New PointF(0, 0)
        Dim nRadius As Integer = 0
        Dim sCallback As String = ""

        ' Read input parameters
        nErrNo = ReadParams(sUser, sPassword, func, frmOut, sText, nLimit, ptCentre, nRadius, sCallback)
        If nErrNo > 0 Then
            If frmOut = outType.XML Then
                XMLError(nErrNo)
            Else
                JSONError(nErrNo, sCallback)
            End If
            Exit Sub
        End If
        If nLimit < 1 Then nLimit = 20 ' If no limit to number of results specified, use 20

        'Connect to the Access database
        MyConn.Open()

        ' Check for valid user name & password
        nErrNo = IsUserValid(MyConn, sUser, sPassword)
        If nErrNo > 0 Then
            If frmOut = outType.XML Then
                XMLError(nErrNo)
            Else
                JSONError(nErrNo, sCallback)
            End If
            If Not MyConn Is Nothing Then MyConn.Close()
            Exit Sub
        End If

        Dim bTest As Boolean = (sUser = "test" And sPassword = "password")

        ' Carry out function
        If func = fnType.STREETLIST Then
            nErrNo = ListStreets(sText, nLimit, ptCentre, nRadius, frmOut, sCallback, bTest)
        ElseIf func = fnType.PLACELIST Then
            nErrNo = ListPlaces(sText, nLimit, frmOut, sCallback, bTest)
        ElseIf func = fnType.CREDITS Then
        End If
        If nErrNo <> 0 Then
            If frmOut = outType.XML Then
                XMLError(nErrNo)
            Else
                JSONError(nErrNo, sCallback)
            End If
        End If

        ' Clean up
        If Not MyConn Is Nothing Then MyConn.Close()


    End Sub

    Public Function ReadParams(ByRef sUser As String, ByRef sPassword As String, ByRef func As fnType, ByRef frmOut As outType, ByRef sText As String, _
                                ByRef nLimit As Integer, ByRef ptCentre As PointF, ByRef nRadius As Integer, _
                                ByRef sCallback As String) As Integer
        ' Parse input parameters
        ' Return: 0 - Success; 1 - Missing username; 2 - Missing password; 3 - Missing/Invalid function, 
        ' 5 - Missing search string; 6 - Invalid centre; 7 - Invalid radius; 8 - Only "S" allowed in demonstration 

        ' Initialise
        Dim nStatus As Integer = 0
        sUser = ""
        sPassword = ""
        sText = ""
        nLimit = 0
        nRadius = 0
        ptCentre = New Point(0, 0)
        frmOut = outType.XML
        sCallback = ""

        ' Load optional output type
        Dim sOutput As String = ""
        sOutput = LCase(Request.QueryString("output"))
        If sOutput = "json" Then
            frmOut = outType.JSON
        ElseIf sOutput = "xml" Then
            frmOut = outType.XML
        End If

        ' Load optional callback function name
        sCallback = Request.QueryString("callback")

        ' Load user ID
        sUser = Request.QueryString("user")
        If sUser = "" Then
            nStatus = 1
        Else
            ' Load password
            sPassword = Request.QueryString("password")
            If sPassword = "" Then
                nStatus = 2
            End If
        End If

        If nStatus = 0 Then
            ' Load function
            Dim sFunction As String = ""
            sFunction = LCase(Request.QueryString("function"))

            If sFunction = "" Then
                nStatus = 3
            ElseIf sFunction = "placelist" Then
                func = fnType.PLACELIST
            ElseIf sFunction = "streetlist" Then
                func = fnType.STREETLIST
            ElseIf sFunction = "getcredit" Then
                func = fnType.CREDITS
            Else
                nStatus = 3
            End If

            ' Load search string as upper-case
            If func = fnType.PLACELIST Or func = fnType.STREETLIST Then
                sText = Request.QueryString("text")
                If sText = "" Then
                    nStatus = 5
                End If
            End If

            If nStatus = 0 Then

                ' If demonstration, only searches on "S" are allowed
                If (sUser = "test") And (sPassword = "password") Then
                    If (sText.Substring(0, 1)).ToUpper() <> "S" Then
                        nStatus = 8
                    End If
                End If

                ' Load optional limit to number of results
                Dim sLimit As String = Request.QueryString("limit")
                If sLimit <> "" Then
                    Try
                        nLimit = Convert.ToInt32(sLimit)
                    Catch
                        nLimit = 0
                    End Try
                End If

                ' Load optional centre and radius
                Dim sCentre As String = Request.QueryString("centre")
                If sCentre <> "" Then
                    Dim Coords As String() = sCentre.Split(",")
                    If Coords.Length = 2 Then
                        Try
                            ptCentre.X = Convert.ToDouble(Coords(0))
                            ptCentre.Y = Convert.ToDouble(Coords(1))
                        Catch
                            nStatus = 6
                        End Try
                    Else
                        nStatus = 6
                    End If
                    Dim sRadius As String = Request.QueryString("radius")
                    If sRadius <> "" Then
                        Try
                            nRadius = Convert.ToInt32(sRadius)
                        Catch
                            nStatus = 7
                        End Try
                        If nRadius < 100 Then
                            nStatus = 7
                        End If
                    End If
                End If
            End If
        End If

        ' Return status code
        Return nStatus

    End Function

    Public Function IsUserValid(ByRef MyConn As OleDbConnection, ByVal sUser As String, ByVal sPassword As String) As Integer
        ' Check if a user name/password is valid
        ' Return: 0 - OK; 3 - Not present; 102 - More than 1 present

        ' Initialise
        Dim nStatus As Integer = 0

        ' Count occurences of name/password in database
        Dim MySQL1 As String = "Select Count(*) FROM UserData WHERE UserName = '" & sUser & "' AND Password = '" & sPassword & "'"

        Dim cmdCount As New OleDbCommand(MySQL1, MyConn)
        Dim nPresent As Int32
        nPresent = Convert.ToInt32(cmdCount.ExecuteScalar())
        cmdCount.Dispose()

        ' Must be exactly one occurence
        If nPresent = 0 Then
            nStatus = 4
        ElseIf nPresent = 1 Then
            nStatus = 0
        Else
            nStatus = 102
        End If

        ' Return result
        Return nStatus

    End Function

    Public Function NormaliseText(ByVal sText As String) As String
        ' Convert text to upper-case; change special chars to spaces; expand "St."

        ' Convert to upper case
        Dim sUpper As String = sText.ToUpper()

        ' Expand Saint prefix
        If sUpper.Length >= 3 Then
            If (sUpper.Substring(0, 3) = "ST.") Or (sUpper.Substring(0, 3) = "ST ") Then
                sUpper = "SAINT " + sUpper.Substring(3)
            End If
        End If

        ' Convert special chars to spaces
        Dim sNorm As String = ""
        Dim bLastSpace As Boolean = True
        Dim iChar As Integer
        For iChar = 0 To sUpper.Length - 1
            If Char.IsLetterOrDigit(sUpper(iChar)) Then
                sNorm = sNorm + sUpper(iChar)
                bLastSpace = False
            Else
                If sUpper(iChar) <> "'" Then
                    If Not bLastSpace Then
                        sNorm = sNorm + " "
                        bLastSpace = True
                    End If
                End If
            End If
        Next iChar

        ' Return result
        Return sNorm

    End Function

    Public Function LoadNormalisedText(ByRef aBuffer() As Byte, ByVal iStart As Integer, ByVal cbText As Integer) As String
        ' Load a normalised string of a given length from a buffer starting at iStart.

        ' Initialise
        Dim sNorm As String = ""
        Dim nScan As Integer = iStart, cbLoad As Integer = 0

        ' Special test for "St." prefix
        Dim bPrefixed As Boolean = False
        If (cbText >= 3) And (iStart + 2 < aBuffer.Length) Then
            If (Char.ToUpper(Chr(aBuffer(iStart))) = "S") And (Char.ToUpper(Chr(aBuffer(iStart + 1))) = "T") Then
                If (Chr(aBuffer(iStart + 2)) = ".") Or (Chr(aBuffer(iStart + 2)) = " ") Then
                    bPrefixed = True
                End If
            End If
        End If

        ' If prefixed, expand into output string
        If bPrefixed Then
            sNorm = "SAINT "
            If cbText <= 6 Then
                sNorm = sNorm.Substring(0, cbText)
                Return sNorm
                Exit Function
            End If
            nScan = iStart + 3
            cbLoad = 6
        End If

        ' Load each character in turn
        Dim bLastSpace As Boolean = True
        While (cbLoad < cbText) And (nScan < aBuffer.Length)
            Dim ch As Char = Chr(aBuffer(nScan))
            If Char.IsLetterOrDigit(ch) Then
                sNorm = sNorm + Char.ToUpper(ch)
                cbLoad = cbLoad + 1
                bLastSpace = False
            Else
                If ch <> "'" Then
                    If Not bLastSpace Then
                        sNorm = sNorm + " "
                        cbLoad = cbLoad + 1
                        bLastSpace = True
                    End If
                End If
            End If
            nScan = nScan + 1
        End While

        ' Return result
        Return sNorm

    End Function

    Public Function ListStreets(ByVal sText As String, ByVal nLimit As Integer, ByVal ptCentre As PointF, _
                               ByVal nRadius As Integer, ByVal frmOut As outType, ByVal sCallback As String, _
                               ByVal bTest As Boolean) As Integer
        ' List Streets given start text
        ' Return 0: OK; 100 - Can't open text file; 101 - Can't open index file

        ' Open the StreetList data and index files
        Dim fStreetList As FileStream, fStreetIndex As FileStream
        Dim sStreetFile As String = "C:\inetpub\vhosts\2676620.cust.peer1.com\bifrost\webservices\App_Data\StreetList.txt"
        Dim sIndexFile As String = "C:\inetpub\vhosts\2676620.cust.peer1.com\bifrost\webservices\App_Data\StreetList.ndx"
        Try
            fStreetList = New FileStream(sStreetFile, FileMode.Open, FileAccess.Read)
        Catch
            Return 100
        End Try
        Try
            fStreetIndex = New FileStream(sIndexFile, FileMode.Open, FileAccess.Read)
        Catch
            fStreetList.Close()
            Return 101
        End Try

        ' Normalise search string
        Dim sSearch As String = NormaliseText(sText)
        Dim aResults As ArrayList
        aResults = New ArrayList()
        If sSearch.Length > 0 Then

            ' Locate first street with given text
            Dim nFilePos As Integer = 0, bFound As Boolean
            bFound = LocateLine(sSearch, fStreetList, fStreetIndex, nFilePos)

            ' Load results
            If bFound Then LoadResults(sSearch, fStreetList, nFilePos, nLimit, ptCentre, nRadius, aResults)
        End If

        ' Output results
        If frmOut = outType.XML Then
            XMLStreetList(sText, aResults)
        Else
            JSONStreetList(sText, aResults, sCallback)
        End If

        ' Clean up
        fStreetList.Close()
        fStreetIndex.Close()
        Return 0

    End Function

    Public Function ListPlaces(ByVal sText As String, ByVal nLimit As Integer, ByVal frmOut As outType, ByVal sCallback As String, _
                                ByVal bTest As Boolean) As Integer
        ' List Places given start text
        ' Return 0: OK; 100 - Can't open text file; 101 - Can't open index file

        ' Open the PlaceList data and index files
        Dim fPlaceList As FileStream, fPlaceIndex As FileStream
        Dim sPlaceFile As String, sIndexFile As String
        sPlaceFile = "C:\inetpub\vhosts\2676620.cust.peer1.com\bifrost\webservices\App_Data\PlaceList.txt"
        sIndexFile = "C:\inetpub\vhosts\2676620.cust.peer1.com\bifrost\webservices\App_Data\PlaceList.ndx"
        Try
            fPlaceList = New FileStream(sPlaceFile, FileMode.Open, FileAccess.Read)
        Catch
            Return 100
        End Try
        Try
            fPlaceIndex = New FileStream(sIndexFile, FileMode.Open, FileAccess.Read)
        Catch
            fPlaceList.Close()
            Return 101
        End Try

        ' Normalise search string
        Dim sSearch As String = NormaliseText(sText)
        Dim aResults As ArrayList
        aResults = New ArrayList()
        If sSearch.Length > 0 Then

            ' Locate first place with given text
            Dim nFilePos As Integer = 0, bFound As Boolean
            bFound = LocateLine(sSearch, fPlaceList, fPlaceIndex, nFilePos)

            ' Load results

            Dim ptCentre As PointF = New PointF(0, 0)     ' Radius searching is not used with places
            If bFound Then LoadResults(sSearch, fPlaceList, nFilePos, nLimit, ptCentre, 0, aResults)
        End If

        ' Output results
        If frmOut = outType.XML Then
            XMLPlaceList(sText, aResults)
        Else
            JSONPlaceList(sText, aResults, sCallback)
        End If

        ' Clean up
        fPlaceList.Close()
        fPlaceIndex.Close()
        Return 0

    End Function

    Public Function CreditRecord(ByRef MyConn As OleDbConnection, ByVal sUser As String, ByVal sPassword As String, ByVal func As fnType, _
                                ByRef nID As Int32, ByRef bNotify As Boolean) As Integer
        ' Get a user purchase record from which a credit will be taken. Load record identifier and clicks used to date.
        ' NB Simply returns zero if function is credit check
        ' Return: 0 - OK, 6 - No credits left, 103 - System error

        ' Initialise
        Dim nErr As Integer = 0
        nID = 0
        Dim bUnlimited As Boolean = False
        bNotify = False
        Dim sToday As String = DateTime.Now.ToString("MM/dd/yyyy")
        Dim sFunc As String = ""
        Select Case func
            Case fnType.PLACELIST
                sFunc = "4"
            Case fnType.STREETLIST
                sFunc = "5"
            Case fnType.CREDITS
                sFunc = ""
            Case Else
                nErr = 103
        End Select

        ' Get all non-expired, non-zero balances for this user and for this type
        Dim sql As String

        'Change date to American style
        'Dim newDate As String = convertDate(sToday)

        sql = "SELECT History.ID, (History.ClicksPurchased - History.ClicksUsed - History.Notify) As NotifyChk, " + _
                "History.UnlimitedClicks " + _
                 "FROM History INNER JOIN UserData ON UserData.ID = History.UserID " + _
                 "WHERE UserData.UserName = '" & sUser & "' AND UserData.Password = '" & sPassword & "' AND " + _
                 "History.Type = " + sFunc + " AND ExpiryDate > #" + sToday + "# AND " + _
                 "((History.ClicksUsed < History.ClicksPurchased) OR History.UnlimitedClicks) " + _
                 "ORDER BY History.ExpiryDate"

        Dim cmd As New OleDbCommand(sql, MyConn)
        Dim list As OleDbDataReader = cmd.ExecuteReader()

        ' If no results, user has no credits left
        If Not list.HasRows() Then
            nErr = 6
        Else
            ' Load oldest or only record identifier for later updating
            list.Read()
            nID = list.GetInt32(0)                 ' Record ID

            ' Check if we should send an email to say clicks low 
            If Not IsDBNull(list.Item(1)) Then
                bNotify = (list.GetInt32(1) = 0)
                If Not IsDBNull(list.Item(2)) Then
                    Dim bNoLimit As Boolean = False
                    bNoLimit = list.GetBoolean(2)
                    If bNoLimit Then bNotify = False
                End If
            End If
        End If

        ' Clean up
        cmd.Dispose()

        ' Return result
        Return nErr

    End Function

    Private Function LocateLine(ByVal sText As String, ByRef fList As FileStream, ByRef fIndex As FileStream, _
                                ByRef nFilePos As Integer) As Boolean
        'Locate first line starting with given text. If not found, return False

        ' Read whole of index file 
        Dim cbIndex As Integer = fIndex.Length()
        Dim aIndex(cbIndex - 1) As Byte
        fIndex.Read(aIndex, 0, cbIndex)
        Dim NewLine As Byte = 10

        ' Find first occurence of first 3 chars of name 
        Dim nLowPos As Integer = 0
        If Not Index3C(sText, aIndex, fList.Length(), nLowPos) Then
            If (sText.Length() >= 3) Or (nLowPos >= fList.Length()) Then
                Return False
            End If
        End If

        ' If supplied text is 1,2 or 3 chars we can exit here
        If sText.Length <= 3 Then
            nFilePos = nLowPos
            Return True
            Exit Function
        End If

        ' Find last occurence of first 3 chars
        Dim sTextNext As String = sText(0) + sText(1) + Chr(Asc(sText(2)) + 1)
        Dim nHighPos As Integer = 0
        Dim bFound As Boolean = Index3C(sTextNext, aIndex, fList.Length(), nHighPos)

        ' Read block from midpoint
        Dim aBuffer(8191) As Byte, cbData As Integer = 0
        Dim bWholeLine As Boolean = True
        Dim iCompare As Integer = 0
        nFilePos = -1
        While (nLowPos <= (nHighPos - sText.Length()))

            ' Read block between high and low limits
            Dim nMidPos As Integer = 0, cbRead As Integer = 0
            If nLowPos + 8192 < nHighPos Then
                nMidPos = Math.Min((nLowPos + nHighPos) / 2, nHighPos - 8192)
                cbRead = 8192
                bWholeLine = False
            Else
                nMidPos = nLowPos
                cbRead = (nHighPos - nLowPos)
                bWholeLine = True
            End If
            fList.Seek(nMidPos, SeekOrigin.Begin)
            cbData = fList.Read(aBuffer, 0, cbRead)

            ' Compare contents of block with target text
            Dim nLineA As Integer = 0, nLineB As Integer = 0
            iCompare = CompareBuffer(sText, aBuffer, cbData, bWholeLine, nLineA, nLineB)
            If (iCompare < 0) Then

                ' Target below this buffer 
                nHighPos = nMidPos + nLineA

            ElseIf iCompare = 0 Then
                ' Target starts this buffer. If we're at lower limit, we're done 
                If nLowPos = nMidPos Then
                    nFilePos = nLowPos + nLineA
                    bWholeLine = True
                    Exit While
                End If

                ' Otherwise set high limit at end of this line (we need first occurence)
                Dim nNewLine As Integer = Array.IndexOf(aBuffer, NewLine, nLineA)
                If nNewLine >= 0 Then
                    nHighPos = nMidPos + nNewLine + 1
                End If

            ElseIf iCompare = 1 Then
                ' Target within this buffer - exit loop
                nFilePos = nMidPos
                Exit While

            Else
                ' Target above this buffer
                nLowPos = nMidPos + nLineB
            End If
        End While
        If nFilePos < 0 Then
            Return False
            Exit Function
        End If

        ' If buffer brackets street name we need a further scan to check its position (may not exist) 
        Dim nPos1 As Integer = ScanBuffer(sText, aBuffer, cbData, bWholeLine)
        If nPos1 < 0 Then
            Return False
            Exit Function
        End If
        nFilePos = nFilePos + nPos1

        ' Target text found
        Return True

    End Function

    Public Function Index3C(ByVal sText As String, ByRef aIndex As Byte(), ByVal cbMaxPos As Integer, ByRef nFilePos As Integer) As Boolean
        ' Given a byte array of 3 chars and 4-byte pointers, look up the text
        ' Return: True - String found; False - Not found

        ' Initialise
        Dim bFound As Boolean = False
        Dim cbFileSize As Integer = aIndex.Length()
        nFilePos = -1
        If cbFileSize < 7 Then
            Return False
        End If
        If sText.Length = 0 Then
            Return False
            Exit Function
        End If
        If sText.Length > 3 Then
            sText = sText.Substring(0, 3)
        End If

        ' Perform binary search
        Dim nFound As Integer = -1
        Dim nLow As Integer = 0
        Dim nLimit As Integer = (cbFileSize / 7) - 1
        While (nLow <= nLimit)
            Dim nMid As Integer = (nLow + nLimit) / 2

            ' Compare and adjust upper or lower limit
            Dim nSub As Integer = nMid * 7
            Dim sTest As String = Convert.ToChar(aIndex(nSub)) + Convert.ToChar(aIndex(nSub + 1)) + Convert.ToChar(aIndex(nSub + 2))
            If sTest < sText Then
                nLow = nMid + 1
            ElseIf sTest > sText Then
                nLimit = nMid - 1
            Else
                ' Found
                nFound = nMid
                bFound = True
                Exit While
            End If
        End While

        ' If string not found, set nFound to lower limit
        If Not bFound Then
            nFound = Math.Max(nLow, 0)
        End If

        ' Load pointer to the text file
        If nFound < (cbFileSize / 7) Then
            Dim b1 As Integer = aIndex((nFound * 7) + 3)
            Dim b2 As Integer = aIndex((nFound * 7) + 4)
            Dim b3 As Integer = aIndex((nFound * 7) + 5)
            Dim b4 As Integer = aIndex((nFound * 7) + 6)
            nFilePos = (b4 << 24) + (b3 << 16) + (b2 << 8) + b1
        Else
            nFilePos = cbMaxPos
        End If

        ' Return status
        Return bFound

    End Function

    Public Function CompareBuffer(ByVal sText As String, ByRef aBuffer() As Byte, ByVal cbBuffer As Integer, _
                                ByVal bWholeLine As Boolean, ByRef nLineA As Integer, ByRef nLineB As Integer) As Integer
        ' Identify the first full lines in a buffer at nLineA and nLineB and compare with a street name
        ' Return: -2 - No lines found; -1 Text below buffer; 0 - Text at start of buffer; +1 - Text within buffer;
        '         +2 - Text beyond buffer

        ' Initialise
        nLineA = -1
        nLineB = -1
        Dim cbText As Integer = sText.Length()
        Dim NewLine As Byte = 10

        ' Find first line
        Dim iChar As Integer = 0
        If bWholeLine Then
            nLineA = 0
        Else
            Dim nFirstNewLine As Integer = Array.IndexOf(aBuffer, NewLine)
            If nFirstNewLine >= 0 Then
                nLineA = nFirstNewLine + 1
            Else
                Return -2
                Exit Function
            End If
        End If

        ' If there is only a single line in the buffer, assume buffer contains a whole line
        If nLineA >= cbBuffer Then nLineA = 0

        ' Load text on first line
        Dim sTest As String = LoadNormalisedText(aBuffer, nLineA, cbText)

        ' Check for street name below buffer
        If sText < sTest Then
            Return -1
            Exit Function
        End If

        ' Check for street name equal to first line
        If sText = sTest Then
            Return 0
            Exit Function
        End If

        ' Load text at start of last line
        Dim nLastNewLine As Integer = Array.LastIndexOf(aBuffer, NewLine, cbBuffer - cbText - 1)
        If nLastNewLine >= 0 Then
            nLineB = nLastNewLine + 1
            sTest = LoadNormalisedText(aBuffer, nLineB, cbText)
        Else
            Return -2
            Exit Function
        End If

        ' Due to skipping non-printing chars the line might be too short, so try again if necessary
        If sTest.Length < cbText Then
            nLastNewLine = Array.LastIndexOf(aBuffer, NewLine, nLineB - 3)
            If nLastNewLine >= 0 Then
                nLineB = nLastNewLine + 1
                sTest = LoadNormalisedText(aBuffer, nLineB, cbText)
            Else
                Return -2
                Exit Function
            End If
        End If

        ' Check for text above buffer
        If sText > sTest Then
            Return 2
        End If

        ' Text contained within buffer
        Return 1

    End Function

    Public Function ScanBuffer(ByVal sText As String, ByRef aBuffer() As Byte, ByVal cbData As Integer, _
                                ByVal bWholeLine As Boolean) As Integer
        ' Scan along a buffer looking for first line starting with given text.
        ' Return line start position or -1 on failure

        ' Initialise
        Dim iStart As Integer = -1
        Dim cbText As Integer = sText.Length()
        Dim NewLine As Byte = 10

        ' Check each line in turn
        Dim iChar As Integer = 0
        While (iChar <= (cbData - cbText - 1))

            ' Scan for start of next line
            If Not bWholeLine Or (iChar > 0) Then
                Dim nNewLine As Integer = Array.IndexOf(aBuffer, NewLine, iChar)
                If nNewLine >= 0 Then
                    iChar = nNewLine + 1
                Else
                    Return -1
                    Exit Function
                End If
            End If

            ' Load text at line start. If it matches, exit
            Dim sTest As String = LoadNormalisedText(aBuffer, iChar, cbText)
            If sTest = sText Then
                iStart = iChar
                Exit While
            Else
                ' No match - loop for next line
                iChar = iChar + cbText
            End If

        End While

        Return iStart

    End Function

    Private Sub LoadResults(ByVal sText As String, ByRef fData As FileStream, ByVal nFilePos As Integer, ByVal nLimit As Integer, _
                            ByRef ptCentre As PointF, ByVal nRadius As Integer, ByRef aResults As ArrayList)
        ' Load street data from a file position

        ' Check if radius searching is enabled
        Dim bRadius As Boolean = (nRadius > 0) And (ptCentre.X <> 0) And (ptCentre.Y <> 0)

        ' Position file
        fData.Seek(nFilePos, SeekOrigin.Begin)

        ' Loop till all matches extracted, or limit reached

        ' Read a block of data
        Dim aData(8191) As Byte
        Dim cbRead As Integer
        cbRead = fData.Read(aData, 0, 8192)
        Dim CRet As Byte = 13

        ' Loop until results list is complete
        Dim iStart As Integer = 0
        While aResults.Count < nLimit

            ' Load string to next carriage return
            Dim sResult As String = ""
            Dim iEnd As Integer = -1
            If iStart >= cbRead Then
                cbRead = fData.Read(aData, 0, 8192)
                If cbRead = 0 Then
                    Exit While
                End If
                iStart = iStart - 8192
            End If
            While (iEnd <= 0)
                iEnd = Array.IndexOf(aData, CRet, iStart)
                If iEnd > 0 Then
                    sResult = sResult + System.Text.Encoding.ASCII.GetString(aData, iStart, iEnd - iStart)
                Else
                    sResult = sResult + System.Text.Encoding.ASCII.GetString(aData, iStart, 8192 - iStart)
                    cbRead = fData.Read(aData, 0, 8192)
                    iStart = 0
                    If cbRead = 0 Then
                        Exit While
                    End If
                End If
            End While

            ' Normalise and test against search string. If not a match, exit
            If sResult.Length >= sText.Length Then
                Dim sTest As String = NormaliseText(sResult)
                sTest = sTest.Substring(0, sText.Length())
                If sTest <> sText Then
                    Exit While
                End If

                ' Apply radius check if required
                Dim bInRadius As Boolean = True
                If nRadius > 0 Then
                    bInRadius = False


                    Dim Fields As String() = sResult.Split(";")
                    Dim iPCD As Integer
                    Dim dLng As Double, dLat As Double
                    For iPCD = 1 To Fields.Length - 1
                        Dim PCD As String() = (Fields(iPCD)).Split(",")
                        Try
                            dLng = Convert.ToDouble(PCD(1))
                            dLat = Convert.ToDouble(PCD(2))
                        Catch
                            Exit For
                        End Try
                        If nRadius > EarthDist(ptCentre.X, ptCentre.Y, dLng, dLat) Then
                            bInRadius = True
                            Exit For
                        End If
                    Next
                End If

                ' Add to result list
                If bInRadius Then aResults.Add(sResult)
            End If

            iStart = iEnd + 2

        End While

    End Sub

    Public Function EarthDist(ByVal long1 As Double, ByVal lat1 As Double, ByVal long2 As Double, ByVal lat2 As Double) As Double
        ' Finds the distance between points on earth ellipsoid in metres.
        ' Taken from Astronomical Algorithms, Jean Meeus, second edition.

        ' Convert degrees to radians
        Dim RAD As Double = (Math.PI / 180.0)
        Dim f As Double = (lat1 + lat2) / 2 * RAD
        Dim g As Double = (lat1 - lat2) / 2 * RAD
        Dim l As Double = (long1 - long2) / 2 * RAD

        Dim sg As Double = Math.Sin(g)
        Dim sl As Double = Math.Sin(l)
        Dim sf As Double = Math.Sin(f)
        Dim s, c, w, r, d, h1, h2

        Dim a As Double = 6377563.396      ' major semi-axis of ellipsoid
        Dim b As Double = 6356256.91       ' minor semi-axis of ellipsoid
        Dim flattn As Double = a / (a - b)
        Dim fl As Double = 1.0 / flattn

        sg = sg * sg
        sl = sl * sl
        sf = sf * sf
        s = sg * (1 - sl) + (1 - sf) * sl
        c = (1 - sg) * (1 - sl) + sf * sl

        If (s = 0.0) Or (c = 0.0) Then Return -1 ' Error

        w = Math.Atan(Math.Sqrt(s / c))
        r = Math.Sqrt(s * c) / w
        d = 2 * w * a
        h1 = (3 * r - 1) / 2 / c
        h2 = (3 * r + 1) / 2 / s

        Return d * (1 + fl * (h1 * sf * (1 - sg) - h2 * (1 - sf) * sg))

    End Function

    Public Sub XMLTag(ByVal sTag As String, ByVal sValue As String)
        ' Output a single XML tag

        ' Create a new XmlTextWriter instance
        Dim XmlWriter As XmlTextWriter = New XmlTextWriter(Response.OutputStream, Encoding.UTF8)

        With XmlWriter
            .WriteStartDocument()
            .WriteStartElement("Result")
            .WriteElementString(sTag, sValue)
            .WriteEndElement()
            .WriteEndDocument()
            .Close()
        End With

    End Sub

    Public Sub XMLError(ByVal nErr As Integer)
        ' Output an XML file notifying an error

        Dim XmlWriter As XmlTextWriter = New XmlTextWriter(Response.OutputStream, Encoding.UTF8)

        ' Write XML header
        With XmlWriter

            .WriteStartDocument()
            .WriteStartElement("Result")

            ' Output error code
            .WriteElementString("ErrCode", nErr.ToString())

            ' Output matching message
            Select Case nErr
                Case 1
                    .WriteElementString("ErrMessage", "Missing user name")

                Case 2
                    .WriteElementString("ErrMessage", "Missing password")

                Case 3
                    .WriteElementString("ErrMessage", "Missing/invalid function")

                Case 4
                    .WriteElementString("ErrMessage", "Invalid user name or password")

                Case 5
                    .WriteElementString("ErrMessage", "Missing search string")

                Case 6
                    .WriteElementString("ErrMessage", "Invalid centre")

                Case 7
                    .WriteElementString("ErrMessage", "Invalid radius")

                Case 8
                    .WriteElementString("ErrMessage", "Only search strings begining with 'S' allowed in testing")

                Case 100
                    .WriteElementString("ErrMessage", "System error: couldn't open data file")

                Case 101
                    .WriteElementString("ErrMessage", "System error: couldn't open index file")

                Case 102
                    .WriteElementString("ErrMessage", "System error: duplicate user")

            End Select

            ' Finish writing
            .WriteEndElement()
            .WriteEndDocument()
            .Close()

        End With

    End Sub

    Public Sub JSONError(ByVal nErr As Integer, ByVal sCallback As String)
        ' Output an JSON block notifying an error

        If String.IsNullOrEmpty(sCallback) Then sCallback = "LoadJSON"

        ' Create a TextWriter instance
        Response.ContentType = "application/json"
        Dim twsJSON As TextWriter = New StreamWriter(Response.OutputStream)

        ' Write opening lines
        With twsJSON
            .Write(sCallback + "(" + vbCrLf)
            .Write("{" + vbCrLf)
            .Write(" ""Results"":" + vbCrLf)
            .Write(" {" + vbCrLf)

            ' Output error code
            .Write(" ""ERRCODE"": """ + nErr.ToString() + """," + vbCrLf)

            ' Output error code and matching message
            Select Case nErr
                Case 1
                    .Write(" ""ERRMESSAGE"": ""Missing user name""" + vbCrLf)

                Case 2
                    .Write(" ""ERRMESSAGE"": ""Missing password""" + vbCrLf)

                Case 3
                    .Write(" ""ERRMESSAGE"": ""Missing/invalid function""" + vbCrLf)

                Case 4
                    .Write(" ""ERRMESSAGE"": ""Invalid user name or password""" + vbCrLf)

                Case 5
                    .Write(" ""ERRMESSAGE"": ""Missing search string""" + vbCrLf)

                Case 6
                    .Write(" ""ERRMESSAGE"": ""Invalid centre""" + vbCrLf)

                Case 7
                    .Write(" ""ERRMESSAGE"": ""Invalid radius""" + vbCrLf)

                Case 8
                    .Write(" ""ERRMESSAGE"": ""Only search strings begining with 'S' allowed in testing""" + vbCrLf)

                Case 100
                    .Write(" ""ERRMESSAGE"": ""System error: couldn't open data file""" + vbCrLf)

                Case 101
                    .Write(" ""ERRMESSAGE"": ""System error: couldn't open index file""" + vbCrLf)

                Case 102
                    .Write(" ""ERRMESSAGE"": ""System error: duplicate user""" + vbCrLf)

            End Select

            ' Finish writing
            .Write(" }" + vbCrLf)
            .Write(")" + vbCrLf)
            .Close()

        End With

    End Sub

    Public Sub XMLStreetList(ByVal sText As String, ByRef aStreetList As ArrayList)
        ' Output an XML file of street names and data

        ' Initialise
        Dim nStreets As Integer = aStreetList.Count()

        ' Create a new XmlTextWriter instance
        Dim XmlWriter As XmlTextWriter = New XmlTextWriter(Response.OutputStream, Encoding.UTF8)

        With XmlWriter

            ' Write XML header
            .WriteStartDocument()
            .WriteComment(" Results for: " + sText + " ")
            .WriteStartElement("Results")

            ' Take each street in turn
            Dim iStreet As Integer
            For iStreet = 0 To (nStreets - 1)
                Dim sStreetData As String = aStreetList(iStreet)
                .WriteStartElement("STREET")

                ' Split into name and postcode list
                Dim Fields As String() = sStreetData.Split(";")

                ' Output street name
                .WriteElementString("NAME", Fields(0))

                ' Output data for each postcode
                If Fields.Length > 1 Then
                    If Fields.Length > 2 Then .WriteStartElement("LOCATIONS")
                    Dim iPCD As Integer
                    For iPCD = 1 To Fields.Length - 1
                        Dim PCDData As String() = Fields(iPCD).Split(",")
                        .WriteStartElement("LOCATION")
                        .WriteElementString("PCD", PCDData(0))
                        If PCDData.Length >= 3 Then
                            .WriteElementString("LNG", PCDData(1))
                            .WriteElementString("LAT", PCDData(2))
                            If PCDData.Length = 5 Then
                                .WriteElementString("GRE", PCDData(3))
                                .WriteElementString("GRN", PCDData(4))
                            End If
                        End If
                        .WriteEndElement()
                    Next
                    If Fields.Length > 2 Then .WriteEndElement()
                End If

                .WriteEndElement()

            Next

            ' Finish writing
            .WriteEndElement()
            .WriteEndDocument()
            .Close()

        End With

    End Sub

    Public Sub JSONStreetList(ByVal sText As String, ByRef aStreetList As ArrayList, ByVal sCallback As String)
        ' Output a JSON structure of street names and data

        ' Initialise
        Dim nStreets As Integer = aStreetList.Count()
        If String.IsNullOrEmpty(sCallback) Then sCallback = "LoadJSON"

        ' Build JSON data string
        Dim sJSON As String = ""
        sJSON = sJSON & "{" & vbCrLf & " ""Results"":" & vbCrLf & " {" & vbCrLf
        sJSON = sJSON & " ""STREET"":" & vbCrLf & " [" & vbCrLf

        ' Take each street in turn
        Dim iStreet As Integer
        For iStreet = 0 To (nStreets - 1)
            Dim sStreetData As String = aStreetList(iStreet)
            sJSON = sJSON & "  {" & vbCrLf

            ' Split into name and postcode list
            Dim Fields As String() = sStreetData.Split(";")

            ' Output street name
            sJSON = sJSON & "  ""NAME"": """ & Fields(0) & """," & vbCrLf

            ' Output data for each postcode
            If Fields.Length > 1 Then
                sJSON = sJSON & "  ""LOCATIONS"":" & vbCrLf
                sJSON = sJSON & "  [" & vbCrLf

                Dim iPCD As Integer
                For iPCD = 1 To (Fields.Length - 1)
                    Dim PCDData As String() = Fields(iPCD).Split(",")
                    sJSON = sJSON & "  {" & vbCrLf
                    sJSON = sJSON & "   ""PCD"": """ & PCDData(0) & """," & vbCrLf
                    If PCDData.Length >= 3 Then
                        sJSON = sJSON & "   ""LNG"": """ & PCDData(1) & """," & vbCrLf
                        sJSON = sJSON & "   ""LAT"": """ & PCDData(2) & """," & vbCrLf
                        If PCDData.Length = 5 Then
                            sJSON = sJSON & "   ""GRE"": """ & PCDData(3) & """," & vbCrLf
                            sJSON = sJSON & "   ""GRN"": """ & PCDData(4) & """" & vbCrLf
                        End If
                    End If
                    If iPCD < Fields.Length - 1 Then
                        sJSON = sJSON & "   }," & vbCrLf
                    Else
                        sJSON = sJSON & "   }" & vbCrLf
                    End If
                Next
                sJSON = sJSON & "  ]" & vbCrLf

            End If
            If iStreet < (nStreets - 1) Then
                sJSON = sJSON & "  }," & vbCrLf
            Else
                sJSON = sJSON & "  }" & vbCrLf
            End If
        Next

        ' Write closing lines
        sJSON = sJSON & " ]" & vbCrLf & " }" & vbCrLf & "}" & vbCrLf

        ' Output data via a TextWriter instance
        Response.ContentType = "application/json"
        Dim twsJSON As TextWriter = New StreamWriter(Response.OutputStream)
        twsJSON.Write(sCallback & "(" & sJSON & ")")
        twsJSON.Close()

    End Sub

    Public Sub XMLPlaceList(ByVal sText As String, ByRef aResults As ArrayList)
        ' Output an XML file of place names and data

        ' Initialise
        Dim nPlaces As Integer = aResults.Count()

        ' Create a new XmlTextWriter instance
        Dim XmlWriter As XmlTextWriter = New XmlTextWriter(Response.OutputStream, Encoding.UTF8)

        With XmlWriter

            ' Write XML header
            .WriteStartDocument()
            .WriteComment(" Results for: " + sText + " ")
            .WriteStartElement("Results")

            ' Take each place in turn 
            Dim iPlace As Integer
            For iPlace = 0 To (nPlaces - 1)
                Dim sLine As String = aResults(iPlace)

                ' Split into name, size code and grid reference
                Dim Fields As String() = sLine.Split(";")

                ' Split lat/long and grid reference into two
                Dim LngLat As String() = Fields(2).Split(",")
                Dim GridRef As String() = Fields(3).Split(",")

                ' Output data
                If Fields.Length >= 2 Then
                    .WriteStartElement("PLACE")
                    .WriteElementString("NAME", Fields(0))
                    .WriteElementString("SIZE", Fields(1))
                    If Fields.Length >= 3 Then
                        .WriteElementString("LNG", LngLat(0))
                        .WriteElementString("LAT", LngLat(1))
                        If Fields.Length = 4 Then
                            .WriteElementString("GRE", GridRef(0))
                            .WriteElementString("GRN", GridRef(1))
                        End If
                    End If
                    .WriteEndElement()
                End If

            Next

            ' Write closing lines
            .WriteEndElement()
            .WriteEndDocument()
            .Close()

        End With

    End Sub

    Public Sub JSONPlaceList(ByVal sText As String, ByRef aResults As ArrayList, ByVal sCallback As String)
        ' Output a JSON structure of place names and data

        ' Initialise
        Dim nPlaces As Integer = aResults.Count()
        If String.IsNullOrEmpty(sCallback) Then sCallback = "LoadJSON"

        ' Construct a string holding JSON data
        Dim sJSON As String = ""
        sJSON = "{" & vbCrLf & " ""Results"":" & vbCrLf & " {" & vbCrLf
        sJSON = sJSON & " ""PLACE"":" & vbCrLf & " [" & vbCrLf

        ' Take each place in turn 
        Dim iPlace As Integer
        For iPlace = 0 To (nPlaces - 1)
            Dim sLine As String = aResults(iPlace)

            ' Split into name, size code and grid reference
            Dim Fields As String() = sLine.Split(";")

            ' Split lat/long and grid reference into two
            Dim LngLat As String() = Fields(2).Split(",")
            Dim GridRef As String() = Fields(3).Split(",")

            ' Output data
            If Fields.Length >= 2 Then
                sJSON = sJSON & "  {" & vbCrLf
                sJSON = sJSON & "  ""NAME"": """ & Fields(0) & """," & vbCrLf
                sJSON = sJSON & "  ""SIZE"": """ & Fields(1) & """," & vbCrLf
                If Fields.Length >= 3 Then
                    sJSON = sJSON & "  ""LNG"": """ & LngLat(0) & """," & vbCrLf
                    sJSON = sJSON & "  ""LAT"": """ & LngLat(1) & """," & vbCrLf
                    If Fields.Length = 4 Then
                        sJSON = sJSON & "  ""GRE"": """ & GridRef(0) & """," & vbCrLf
                        sJSON = sJSON & "  ""GRN"": """ & GridRef(1) & """" & vbCrLf
                    End If
                End If
                If iPlace < (nPlaces - 1) Then
                    sJSON = sJSON & "  }," & vbCrLf
                Else
                    sJSON = sJSON & "  }" & vbCrLf
                End If
            End If
        Next

        ' Add closing lines
        sJSON = sJSON & " ]" & vbCrLf & " }" & vbCrLf & "}" & vbCrLf

        ' Output data via a TextWriter instance
        Response.ContentType = "application/json"
        Dim twsJSON As TextWriter = New StreamWriter(Response.OutputStream)
        twsJSON.Write(sCallback & "(" & sJSON & ")")
        twsJSON.Close()


    End Sub

    Public Sub XMLDebug(ByVal sMess As String)
        ' Output an XML file with a debug message

        ' Create a new XmlTextWriter instance
        Dim XmlWriter As XmlTextWriter = New XmlTextWriter(Response.OutputStream, Encoding.UTF8)

        With XmlWriter
            .WriteStartDocument()
            .WriteStartElement("Result")
            .WriteElementString("Debug", sMess)
            .WriteEndElement()
            .WriteEndDocument()
            .Close()
        End With

    End Sub

End Class
